package jmine.tec.rpc.client.provider;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import jmine.tec.rpc.RPCMessages;
import jmine.tec.rpc.common.RPCException;
import jmine.tec.rpc.common.entity.EntityRepresentation;
import jmine.tec.rpc.common.entity.EntityRest;
import jmine.tec.rpc.common.provider.RestServiceDiscovery;
import jmine.tec.rpc.common.provider.RestServiceProvider;
import jmine.tec.rpc.common.provider.ServiceInfo;
import jmine.tec.rpc.common.provider.ServiceUnavailableException;

/**
 * Provedor de serviços REST, implementação a ser utilizada pelo lado cliente.
 * 
 * @author lundberg
 */
public class ClientRestServiceProvider implements RestServiceProvider {

    private ClientStubFactory stubFactory;

    private RestServiceDiscovery discovery;

    private final Map<Class<?>, Object> implementors = new ConcurrentHashMap<Class<?>, Object>();

    private final Map<Class<?>, EntityRest<?>> entityImplementors = new ConcurrentHashMap<Class<?>, EntityRest<?>>();

    /**
     * {@inheritDoc}
     */
    public <S> S provideByClass(Class<S> service) {
        if (!this.implementors.containsKey(service)) {
            this.findServiceAndCreateStub(service);
        }
        return service.cast(this.implementors.get(service));
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public <R extends EntityRepresentation> EntityRest<R> provideByRepresentation(Class<R> representation) {
        if (!this.entityImplementors.containsKey(representation)) {
            this.findServiceAndCreateStubForRepresentation(representation);
        }
        return (EntityRest<R>) this.entityImplementors.get(representation);
    }

    /**
     * {@inheritDoc}
     */
    public <R extends EntityRepresentation> boolean providesByRepresentation(Class<R> representation) {
        if (this.entityImplementors.containsKey(representation)) {
            return true;
        }
        try {
            ServiceInfo<? extends EntityRest<R>> info = this.discovery.getRepresentationServiceInfo(representation);
            this.createEntityServiceStub(representation, info);
            return true;
        } catch (ServiceUnavailableException e) {
            return false;
        }
    }

    /**
     * @param <S> tipo de serviço
     * @param service service
     */
    private <S> void findServiceAndCreateStub(Class<S> service) {
        try {
            this.createStub(this.discovery.getServiceInfo(service));
        } catch (ServiceUnavailableException e) {
            throw new RPCException(RPCMessages.REMOTE_SERVICE_UNAVAILABLE.create(service.getName()), e);
        }
    }

    /**
     * Busca o serviço adequado à representação e cria um stub. Joga exception caso não seja possível.
     * 
     * @param <R> tipo de representação
     * @param representation representation
     */
    private <R extends EntityRepresentation> void findServiceAndCreateStubForRepresentation(Class<R> representation) {
        try {
            ServiceInfo<? extends EntityRest<R>> info = this.discovery.getRepresentationServiceInfo(representation);
            this.createEntityServiceStub(representation, info);
        } catch (ServiceUnavailableException e) {
            throw new RPCException(RPCMessages.ENTITY_REST_SERVICE_NOT_FOUND.create(representation.getName()), e);
        }
    }

    /**
     * Cria e registra o stub para o serviço da representação
     * 
     * @param <R> tipo de representação
     * @param representation representation
     * @param info service info
     */
    protected <R extends EntityRepresentation> void createEntityServiceStub(Class<R> representation,
            ServiceInfo<? extends EntityRest<R>> info) {
        EntityRest<R> stub = this.stubFactory.create(info.getService(), info.getPrefix());
        this.implementors.put(info.getService(), stub);
        this.entityImplementors.put(representation, stub);
    }

    /**
     * Cria um stub para o serviço
     * 
     * @param <S> tipo de serviço
     * @param info service info
     */
    private <S> void createStub(ServiceInfo<S> info) {
        this.implementors.put(info.getService(), this.stubFactory.create(info.getService(), info.getPrefix()));
    }

    /**
     * @param stubFactory the stubFactory to set
     */
    public void setStubFactory(ClientStubFactory stubFactory) {
        this.stubFactory = stubFactory;
    }

    /**
     * @param discovery the discovery to set
     */
    public void setDiscovery(RestServiceDiscovery discovery) {
        this.discovery = discovery;
    }
}
